OpenFisca-US

This is a working prototype of an OpenFisca-powered microsimulation model for the US tax system. Note that this is a very minimal implementation with only a few variables implemented, and figures therefore will not match aggregates.

Interface

The general interface is largely inherited from tools developed for OpenFisca-UK.

Populations

Population-level analyses can use Microsimulation.

from openfisca_us import Microsimulation
import plotly.express as px

sim = Microsimulation()
years = list(range(2018, 2028))
incomes = [sim.calc("Taxes", year).sum() for year in years]

px.line(x=years, y=incomes).update_layout(
    template="plotly_white",
    xaxis_title="Year",
    yaxis_title="Tax Aggregate",
    xaxis_tickvals=years,
)

This class also has in-built tools for marginal tax rate calculation, including handling for MTR calculation on variables of different entities (e.g. MTR of tax unit tax liability with respect to individual variables).

mtr = (
    sim.deriv("Taxes", wrt="earned")
    .groupby(sim.calc("earned"))
    .mean()
    .rolling(100)
    .mean()
)

px.line(mtr[mtr.index < 1e6]).update_layout(
    template="plotly_white",
    xaxis_title="Earned income",
    yaxis_title="Effective MTR",
    showlegend=False,
)

Individuals

Hypothetical tax scenarios are simple to calculate with IndividualSim.

from openfisca_us import IndividualSim
import pandas as pd

single_filer = IndividualSim()


def create_single_blind_tu(*args):
    sim = IndividualSim(*args, year=2021)
    sim.add_person(name="person")
    sim.add_taxunit(head="person", MARS="SINGLE", blind_head=True)
    sim.vary("earned")
    return sim


single_filer = create_single_blind_tu()
# a useful OpenFisca feature is the equal treatment of input, intermediate and output variables
# any operation can be done on either, e.g. MTR calculation wrt an intermediate variable
# like below
single_filer.vary("earned")
results = pd.DataFrame(
    {
        "Earned income": single_filer.calc("earned")[0],
        "Taxes": single_filer.calc_deriv("Taxes", wrt="earned"),
        "Standard deduction": single_filer.calc_deriv(
            "standard_deduction", wrt="earned"
        ),
    }
)
px.line(
    results, x="Earned income", y=["Taxes", "Standard deduction"]
).update_layout(
    yaxis_tickformat="%",
    template="plotly_white",
    xaxis_title="Earned income",
    yaxis_title="Derivative",
)
single_filer = create_single_blind_tu()
# a useful OpenFisca feature is the equal treatment of input, intermediate and output variables
# any operation can be done on either, e.g. MTR calculation wrt an intermediate variable
# like below
single_filer.vary("earned")
results = pd.DataFrame(
    {
        "Earned income": single_filer.calc("earned")[0],
        "Taxes": single_filer.calc("Taxes")[0],
        "Standard deduction": single_filer.calc("standard_deduction")[0],
        "After-tax income": single_filer.calc("AfterTaxIncome")[0],
    }
)
px.line(
    results,
    x="Earned income",
    y=["Taxes", "Standard deduction", "After-tax income"],
).update_layout(
    yaxis_tickprefix="$",
    template="plotly_white",
    xaxis_title="Earned income",
    yaxis_title="Amount",
)

Reforms

Parametric reforms can now be specified from a single YAML file.

from openfisca_us.reforms import reform_from_file

# a reform that multiplies the basic standard deduction by 10
reform = reform_from_file("tax_cut.yaml")

baseline = Microsimulation()
reformed = Microsimulation(reform)

years = list(range(2018, 2028))
revenues = [
    reformed.calc("Taxes", year).sum() - baseline.calc("Taxes", year).sum()
    for year in years
]

px.line(x=years, y=revenues).update_layout(
    template="plotly_white",
    xaxis_title="Year",
    yaxis_title="Reform revenue",
    xaxis_tickvals=years,
)

One of the most useful features of OpenFisca is its Python interface for reforms, allowing multi-reform analyses to be calculated procedurally in code.

from openfisca_us.reforms import parametric_reform


def increase_standard_deduction(amount):
    def modify_params(parameters):
        std_ded = parameters.tax.deductions.standard.amount.filer
        for MARS_type in std_ded.children:
            current_value = std_ded.children[MARS_type].get_at_instant(
                "2020-01-01"
            )
            new_value = current_value + amount
            std_ded.children[MARS_type].update(
                period="year:2020:10", value=new_value
            )
        return parameters

    return parametric_reform(modify_params)


results = []
# for each level of StD increase:
baseline = create_single_blind_tu()
for amount in range(1000, 200000, 5000):
    # generate two hypothetical scenario simulations
    reform = create_single_blind_tu(increase_standard_deduction(amount))
    # add all the results as rows to a dataframe, with each row labelled with its reform info
    results += [
        pd.DataFrame(
            {
                "Earned income": baseline.calc("earned")[0],
                "Taxes (Baseline)": baseline.calc("Taxes")[0],
                "Taxes (Reform)": reform.calc("Taxes")[0],
                "SD increase": amount,
            }
        )
    ]

results = pd.concat(results)

px.line(
    results,
    x="Earned income",
    y=["Taxes (Baseline)", "Taxes (Reform)"],
    animation_frame="SD increase",
).update_layout(
    yaxis_tickprefix="$",
    template="plotly_white",
    xaxis_title="Earned income",
    yaxis_title="Amount",
)